#include "encoding.h"

#include <assert.h>
#include <errno.h>
#include <string.h>

static const char* encoding_names[][3] = {
    { "ASCII", NULL },
    { "UTF-8", "UTF8", NULL },
    { "UTF-16", "UTF16", NULL },
    { "UTF-32", "UTF32", NULL },
    { "CP1251", "WINDOWS-1251", NULL },
    { "CP866", NULL },
    { "KOI8-R", "KOI8R", NULL }
};

G_STATIC_ASSERT(sizeof(encoding_names) / sizeof(*encoding_names) ==
                ENCODINGS_COUNT);

static GIConv iconv_cache[] = {
    (GIConv)-1, (GIConv)-1, (GIConv)-1, (GIConv)-1,
    (GIConv)-1, (GIConv)-1, (GIConv)-1
};

G_STATIC_ASSERT(sizeof(iconv_cache) / sizeof(*iconv_cache) == ENCODINGS_COUNT);

static GIConv get_iconv(Encoding enc) {
    if(iconv_cache[enc] == (GIConv)-1)
        iconv_cache[enc] = g_iconv_open("UTF-8", get_encoding_name(enc));
    g_assert(iconv_cache[enc] != (GIConv)-1);
    return iconv_cache[enc];
}

static inline void grow(GString* str) {
    g_string_set_size(str, str->len * 2);
}

GString* decode_string(const gchar* input, gsize input_len, Encoding encoding) {
    assert(encoding >= 0 && encoding < ENCODINGS_COUNT);

    static const size_t ENCODE_INITIAL_BUFFER_SIZE = 8 * 1024;

    if(input_len == 0)
        return g_string_sized_new(0);

    GString* result = g_string_sized_new(ENCODE_INITIAL_BUFFER_SIZE);
    g_string_set_size(result, ENCODE_INITIAL_BUFFER_SIZE);
    const GIConv iconv = get_iconv(encoding);
    gchar* inbuf = (gchar*)input;
    gsize inbytes_left = input_len;
    gsize bytes_written = 0;
    while(inbytes_left > 0) {
        gchar* outbuf = result->str + bytes_written;
        const gsize bufsize = result->len - bytes_written;
        gsize outbytes_left = bufsize;
        gsize res = g_iconv(iconv, &inbuf, &inbytes_left, &outbuf,
                            &outbytes_left);
        bytes_written += bufsize - outbytes_left;
        if(res == (gsize)-1) {
            if(errno == E2BIG)
                grow(result);
            else {
                if(result->len - bytes_written < 3)
                    grow(result);
                result->str[bytes_written++] = '\xEF';
                result->str[bytes_written++] = '\xBF';
                result->str[bytes_written++] = '\xBD';
                ++inbuf;
                --inbytes_left;
            }
        }
    }
    g_string_set_size(result, bytes_written);
    for(gchar* p = result->str, *end = result->str + result->len; p != end; ++p)
        if(*p == '\0') *p = ' ';

    return result;
}

Encoding determine_encoding_by_content_type(const GString* ct) {
    const char* semicolon = strchr(ct->str, ';');
    if(!semicolon)
        return UNKNOWN_ENCODING;
    for(++semicolon; *semicolon && g_ascii_isspace(*semicolon); ++semicolon);
    if(g_ascii_strncasecmp(semicolon, "charset", 7) != 0)
        return UNKNOWN_ENCODING;
    for(semicolon += 7; *semicolon && g_ascii_isspace(*semicolon); ++semicolon);
    if(*semicolon++ != '=')
        return UNKNOWN_ENCODING;
    while(*semicolon && g_ascii_isspace(*semicolon)) ++semicolon;
    return get_encoding_by_name(semicolon);
}

const char* get_encoding_name(Encoding enc) {
    assert(enc >= 0 && enc < ENCODINGS_COUNT);
    return encoding_names[enc][0];
}

Encoding get_encoding_by_name(const char* encoding_name) {
    for(size_t i = 0; i < ENCODINGS_COUNT; ++i)
        for(const char* const* names = encoding_names[i]; *names; ++names)
            if(g_ascii_strcasecmp(encoding_name, *names) == 0)
                return i;
    return UNKNOWN_ENCODING;
}
